Skip to content

Add support for local variables in find-references#1231

Merged
lionel- merged 7 commits into
mainfrom
oak/find-refs-intra-file
May 27, 2026
Merged

Add support for local variables in find-references#1231
lionel- merged 7 commits into
mainfrom
oak/find-refs-intra-file

Conversation

@lionel-
Copy link
Copy Markdown
Contributor

@lionel- lionel- commented May 25, 2026

Branched from #1230

Part of posit-dev/positron#13749
Part of #1149

Adds a within-file Oak path to the existing find-references handler of the Ark LSP.

Same deal as for goto-definition: within-file behaviour is based on the semantic index and precise, cross-file is handled by the legacy handler and is approximate (matches any symbol of the same name without filtering based on expected definition).

Copy link
Copy Markdown
Contributor

@DavisVaughan DavisVaughan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did some basic testing and it seems to work

pub fn child_scopes(&self, scope: ScopeId) -> ChildScopesIter<'_> {
let descendants = &self.scopes[scope].descendants;
ChildScopesIter {
pub fn child_scope_ids(&self, scope_id: ScopeId) -> ChildScopeIdsIter<'_> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i approve of this naming!

Comment on lines +202 to +204
pub fn scope_ids(&self) -> impl Iterator<Item = ScopeId> + '_ {
self.scopes.iter().map(|(id, _)| id)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it would be more useful and general to just expose this?

pub fn scopes(&self) -> &IndexVec<ScopeId, Scope>

Or do you purposefully try not to directly expose the IndexVec?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm no we do expose IndexVec elsewhere, so i do feel like this might be more useful to expose?

pub fn uses(&self, scope: ScopeId) -> &IndexVec<UseId, Use> {

Comment on lines 222 to +224
scope: ScopeId,
) -> Option<(ScopeId, DefinitionId, &Definition)> {
for ancestor in self.ancestor_scopes(scope) {
for ancestor in self.ancestor_scope_ids(scope) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
scope: ScopeId,
) -> Option<(ScopeId, DefinitionId, &Definition)> {
for ancestor in self.ancestor_scopes(scope) {
for ancestor in self.ancestor_scope_ids(scope) {
scope_id: ScopeId,
) -> Option<(ScopeId, DefinitionId, &Definition)> {
for ancestor_id in self.ancestor_scope_ids(scope) {

probably would be useful to do a Claude pass once all of this is merged that does a mass renaming for consistency

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add it to the salsa post-pr bullet list

Comment thread crates/oak_ide/src/identifier.rs Outdated
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the changes in identifier.rs make this feeeeel more smooth!

Comment on lines 175 to +193

// Check and see if we got an identifier. If we didn't, we might need to use
// some heuristics to look around. Unfortunately, it seems like if you double-click
// to select an identifier, and then use Right Click -> Find All References, the
// position received by the LSP maps to the _end_ of the selected range, which
// is technically not part of the associated identifier's range. In addition, we
// can't just subtract 1 from the position column since that would then fail to
// resolve the correct identifier when the cursor is located at the start of the
// identifier.
// Zero-width range queries at an identifier boundary return the
// wrapping node rather than the identifier itself. If the cursor is at
// the trailing edge of a selection (column past the last character),
// retry one column back. If it's at the leading edge (column on the
// first character), retry one column forward.
if !node.is_identifier() && point.column > 0 {
let point = Point::new(point.row, point.column - 1);
node = ast
let back = Point::new(point.row, point.column - 1);
if let Some(retry) = ast
.root_node()
.descendant_for_point_range(point, point)
.into_result()?;
.descendant_for_point_range(back, back)
.filter(|n| n.is_identifier())
{
node = retry;
}
}
if !node.is_identifier() {
let fwd = Point::new(point.row, point.column + 1);
if let Some(retry) = ast
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i dont remember the details right now but id be pretty hesitant to change this right now

im not sure we really have a big suite of tests for this, but it seems like this was from some hard fought debugging based on the comment

can we just leave it as is till we remove it for good?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm just expanding the workaround, with a test. I've added a test for the original workaround too. It doesn't seem like a very risky path, so I think I'd prefer to go ahead with the change. Otherwise we'll need to (a) keep a hole in the impl, and (b) keep a hole in the test coverage, both with fixmes.

Comment on lines +88 to +94
// Some LSP clients send the cursor one past the last character of a
// selected identifier (typical for double-click then "find references").
// We exercise this through the cross-file fallback because `mutate` is
// unbound: the intra-file pass returns nothing, and `build_context`'s
// boundary retry pulls the cursor back one column onto the identifier
// before the textual walk runs.
let dir = tempfile::tempdir().unwrap();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we also exercise this in the intra file case?

it is an important use case, because its probably how i do find references 99% of the time (double click first, which puts my flashing cursor at the end of mutate, then right click and "find references")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a test, and a fix because the test was failing.

We now have a rudimentary version of r-a's pick_best_token() that snaps to a neighbouring identifier token when the offset sits in between tokens.

Comment thread crates/oak_ide/src/find_references.rs Outdated
Comment on lines +81 to +105
// Walk all uses in every scope and check for each use of the same name
// whether its binding set intersects the target.
for scope_id in index.scope_ids() {
let symbols = index.symbols(scope_id);
let Some(symbol_id) = symbols.id(&name) else {
// The scope doesn't have any uses for that symbol
continue;
};

for (use_id, use_site) in index.uses(scope_id).iter() {
if use_site.symbol() != symbol_id {
continue;
}
let intersects = index
.reaching_definitions(scope_id, use_id)
.any(|d| target_defs.contains(&d));
if !intersects {
continue;
}
results.push(FileRange {
file: pos.file.clone(),
range: use_site.range(),
});
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whew, find_references() is complicated! So IIUC, with:

foo <- function() {}

foo() # <- find references from here

bar()

foo()

We first classify() foo to figure out that it is a Identifier::Use and collect its reaching_definitions() of foo <- function() {} as target_defs

But that's really only the first part of find_references(). The real part is figuring out where else this thing is used.

And I guess we don't directly store that info in the SemanticIndex on an individual Definition, so we have to hunt for it.

So we scan the file top to bottom looking for uses of the foo symbol, and we see if its set of reaching definitions match ours, and those become part of the final result.


Now it totally makes sense why other groups do a textual scan as part of this feature.

Scanning through every single file in the workspace from top to bottom would nuke performance. So the textual scan can probably at least do the symbol matching loop for you across all files very quickly, and then we'd just be in charge of matching up the uses of all of the hits with our target_defs!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without the textual search I guess we'd maintain a map of some sort.

Comment thread crates/oak_ide/src/find_references.rs
@lionel- lionel- force-pushed the oak/goto-def-intra-file branch from 7e6ad7c to 6fa14ce Compare May 27, 2026 20:57
@lionel- lionel- force-pushed the oak/find-refs-intra-file branch from e150326 to 2bc2610 Compare May 27, 2026 20:58
Base automatically changed from oak/goto-def-intra-file to main May 27, 2026 22:49
@lionel- lionel- force-pushed the oak/find-refs-intra-file branch from 82d3822 to 014546a Compare May 27, 2026 22:50
@lionel- lionel- merged commit 9672c37 into main May 27, 2026
17 checks passed
@lionel- lionel- deleted the oak/find-refs-intra-file branch May 27, 2026 22:50
@github-actions github-actions Bot locked and limited conversation to collaborators May 27, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants